﻿using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Web.Caching;
using Citi.Helpers;

namespace Citi.Import.Util
{
    public static class Conversion
    {
        public enum Validation
        {
            Validated,
            Failed,
            ManuallyValidated
        }

        public static string TransformColumnName(string str)
        {
            var sb = new StringBuilder();
            foreach (var c in str)
            {
                if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
                    sb.Append(c);
            }
            return sb.ToString().ToUpper();
        }

        #region Convert From File to Object

        private static bool ValidateFile(string file, ref List<Error> errors)
        {
            errors = new List<Error>();

            if (File.Exists(file)) return true;

            errors.Add(new Error { ErrorDescription = string.Format("File not found at {0}", file), Line = 0 });

            return false;
        }

        public static List<T> ConvertFromTxt<T>(List<Definition<T>> definitions, string txtPath, char separator, ref List<Error> errors, bool hasHeader = true) where T : class
        {
            if (!ValidateFile(txtPath, ref errors))
                return null;

            var lines = ReadLines(txtPath).ToList();

            var headers = new List<string>();

            if (hasHeader)
            {
                headers = lines[0].Split(separator).Select(x => TransformColumnName(x).ToUpper()).ToList();
                lines.RemoveAt(0);
            }

            var fields = lines.Select(x => x.Split(separator).ToList()).ToList();

            return Convert(definitions, fields, headers, ref errors);
        }

        public static List<T> ConvertFromTxt<T>(List<Definition<T>> definitions, string txtPath, char separator, bool hasHeader = true) where T : class
        {
            List<Error> errors = null;
            return ConvertFromTxt(definitions, txtPath, separator, ref errors, hasHeader);
        }

        public static List<T> ConvertFromExcel<T>(List<Definition<T>> definitions, string excelPath, ref List<Error> errors, string workSheetName = null, int ignoreFirstLines = 0) where T : class
        {
            var dataTable = ReadExcel(excelPath, ref errors, workSheetName, ignoreFirstLines);
            return Convert(definitions, dataTable, ref errors);
        }

        public static DataTable ReadExcel(string excelPath, ref List<Error> errors, string workSheetName = null, int ignoreFirstLines = 0)
        {
            if (!ValidateFile(excelPath, ref errors))
                return null;

            return ExcelNpoiHelper.GetData(excelPath, workSheetName, false, ignoreFirstLines);
            //using (var reader = new ExcelReader(excelPath, true, true))
            //   return reader.GetData(workSheetName, false, ignoreFirstLines);
        }

        public static List<T> ConvertFromExcel<T>(List<Definition<T>> definitions, string excelPath, string workSheetName = null, int ignoreFirstLines = 0) where T : class
        {
            List<Error> errors = null;
            return ConvertFromExcel(definitions, excelPath, ref errors, workSheetName, ignoreFirstLines);
        }

        public static List<T> ConvertFromList<T>(List<Definition<T>> definitions, List<List<string>> fields, ref List<Error> errors) where T : class
        {
            return Convert(definitions, fields, null, ref errors);
        }

        public static List<T> Convert<T>(List<Definition<T>> definitions, List<string> headers, List<List<string>> fields) where T : class
        {
            List<Error> errors = null;
            return Convert(definitions, fields, headers, ref errors);
        }

        public static List<T> Convert<T>(List<Definition<T>> definitions, List<List<string>> fields, List<string> headers, ref List<Error> errors) where T : class
        {
            var models = (List<T>)Activator.CreateInstance(typeof(List<T>));
            errors = errors ?? new List<Error>();

            foreach (var field in fields)
            {
                Error error = null;
                var converted = Convert(definitions, field, fields.IndexOf(field), headers, ref error);

                if (error != null)
                    errors.Add(error);
                else
                    models.Add(converted);
            }

            return models.Where(x => x != null).ToList();
        }

        public static List<T> Convert<T>(List<Definition<T>> definitions, DataTable dataTable) where T : class
        {
            List<Error> errors = null;

            return Convert(definitions, dataTable, ref errors);
        }

        public static List<T> Convert<T>(List<Definition<T>> definitions, DataTable dataTable, ref List<Error> errors) where T : class
        {
            var models = (List<T>)Activator.CreateInstance(typeof(List<T>));
            errors = errors ?? new List<Error>();

            var headers = (from DataColumn column in dataTable.Columns select TransformColumnName(column.ColumnName).ToUpper()).ToList();

            foreach (DataRow dataRow in dataTable.Rows)
            {
                Error error = null;
                var fields = dataRow.ItemArray.ToList().Select(x => x != null ? x.ToString() : string.Empty).ToList();

                if (fields.All(string.IsNullOrEmpty))
                    break;

                var converted = Convert(definitions, fields,
                    dataTable.Rows.IndexOf(dataRow), headers, ref error);

                if (error != null)
                    errors.Add(error);
                else
                    models.Add(converted);
            }

            return models.Where(x => x != null).ToList();
        }

        private static T Convert<T>(IEnumerable<Definition<T>> definitions, List<string> fields, int line, List<string> headers, ref Error error) where T : class
        {
            var model = (T)Activator.CreateInstance(typeof(T));
            error = null;

            foreach (var definition in definitions.Where(x => x.TypeConversion != TypeConversion.Write))
            {
                if (!string.IsNullOrEmpty(definition.ColumnName) && definition.Position > fields.Count() - 1)
                {
                    error = new Error
                        {
                            ErrorDescription = string.Format("The field number {0} does not exist. Headers count: {1}", definition.Position, fields.Count()),
                            Line = line,
                            Fields = fields
                        };
                    return null;
                }

                try
                {
                    string errorDescription;
                    var field = definition.ColumnName == null
                        ? fields[definition.Position]
                        : fields[headers.IndexOf(TransformColumnName(definition.ColumnName.ToUpper()))];

                    var convertedValue = definition.ConvertValue(ref model, field, fields, headers, out errorDescription, TypeConversion.Read);

                    if (definition.CustomConversionMethod != null && convertedValue == null && string.IsNullOrEmpty(errorDescription))
                        return null;

                    if (convertedValue == null && !string.IsNullOrEmpty(errorDescription))
                    {
                        error = new Error { ErrorDescription = errorDescription, Line = line, Fields = fields };
                        return null;
                    }

                    if (convertedValue != null && convertedValue.GetType() == typeof(Validation)) continue;

                    definition.Property.SetValue(model, convertedValue, null);
                }
                catch (Exception e)
                {
                    error = new Error
                    {
                        ErrorDescription = string.Format("Unespected error occurred: {0}", e.Message),
                        Line = line,
                        Fields = fields
                    };
                }
            }

            return model;
        }

        private static IEnumerable<String> ReadLines(string path)
        {
            if (!File.Exists(path))
                throw new FileNotFoundException(path);

            using (var reader = new StreamReader(path, Encoding.GetEncoding("ISO-8859-1")))
            {
                string line;

                while ((line = reader.ReadLine()) != null)
                {
                    yield return line;
                }
            }
        }

        #endregion

        public static PropertyInfo GetPropertyInfo<T, TProperty>(Expression<Func<T, TProperty>> propertyLambda)
        {
            var stack = new Stack<string>();

            MemberExpression me;
            switch (propertyLambda.Body.NodeType)
            {
                case ExpressionType.Convert:
                case ExpressionType.ConvertChecked:
                    var ue = propertyLambda.Body as UnaryExpression;
                    me = ((ue != null) ? ue.Operand : null) as MemberExpression;
                    break;
                default:
                    me = propertyLambda.Body as MemberExpression;
                    break;
            }

            while (me != null)
            {
                stack.Push(me.Member.Name);
                me = me.Expression as MemberExpression;
            }

            return typeof(T).GetProperty(string.Join(".", stack.ToArray()));
        }

        #region Convert From Object to File

        public static IEnumerable<string> FillTextFromModel<T>(T model, IEnumerable<Definition<T>> definitions)
        {
            var errors = new List<Error>();
            return FillTextFromModel(model, definitions, null, 0, ref errors);
        }

        private static IEnumerable<string> FillTextFromModel<T>(T model, IEnumerable<Definition<T>> definitions, List<string> headers,
            int index, ref List<Error> errors)
        {
            var definitionsOrdered = definitions.Where(x => x.TypeConversion != TypeConversion.Read).OrderBy(x => x.Position);
            var fields = new List<string>();
            try
            {
                foreach (var definition in definitionsOrdered)
                {
                    var value = definition.Property.GetValue(model, null);
                    string errorDescription;

                    var converted = definition.ConvertValue(ref model, value, null, headers, out errorDescription, TypeConversion.Write);

                    if (errorDescription != null)
                    {
                        if (value != null)
                            fields.Add(value.ToString());

                        errors.Add(new Error { ErrorDescription = errorDescription, Fields = fields, Line = index });

                        return null;
                    }

                    fields.Add(converted != null ? converted.ToString() : string.Empty);
                }

                return fields;
            }
            catch (Exception e)
            {
                errors.Add(new Error
                {
                    ErrorDescription = string.Format("Unespected error occurred: {0}", e.Message),
                    Line = index,
                    Fields = fields
                });

                return null;
            }
        }

        public static void ConvertToTxt<T>(List<T> models, IEnumerable<Definition<T>> definitions, string txtPath, char separator, out List<Error> errors, bool writeHeader = true) where T : class
        {
            File.WriteAllText(txtPath, string.Empty);
            var definitionsOrdered = definitions.OrderBy(x => x.Position);
            List<string> headers = null;
            errors = new List<Error>();

            if (writeHeader)
            {
                headers = definitionsOrdered.Select(x => x.ColumnName).ToList();
                File.WriteAllText(txtPath, string.Join(separator.ToString(), headers));
            }

            foreach (var model in models)
            {
                var fields = FillTextFromModel(model, definitionsOrdered, headers, models.IndexOf(model), ref errors);
                File.WriteAllText(txtPath, string.Join(separator.ToString(), fields));
            }
        }

        public static void ConvertToTxt<T>(List<T> models, IEnumerable<Definition<T>> definitions, string txtPath, char separator, bool hasHeader = true) where T : class
        {
            List<Error> errors;
            ConvertToTxt(models, definitions, txtPath, separator, out errors, hasHeader);
        }


        #endregion
    }
}
